﻿using System.Collections.Generic;
using Godot.core.io;

namespace Godot.core.os
{
    using CreateFunc = System.Func<FileAccess>;

    public abstract class FileAccess
    {
        public enum AccessType
        {
            ACCESS_RESOURCES,
            ACCESS_USERDATA,
            ACCESS_FILESYSTEM,
            ACCESS_MAX
        }

        public enum ModeFlags
        {
            READ = 1,
            WRITE = 2,
            READ_WRITE = 3,
        }

        private static FileAccess __create_builtin<T>() where T : FileAccess, new()
        {
            return new T();
        }

	    protected string _fix_path(string p_path)
        {
            //helper used by file accesses that use a single filesystem
	        string r_path = p_path.Replace("\\", "/");

	        switch(__access_type) 
            {
		        case AccessType.ACCESS_RESOURCES: {
			        if (Globals.singleton != null) 
                    {
				        if (r_path.StartsWith("res://")) 
                        {
					        string resource_path = Globals.singleton.get_resource_path();
					        if (resource_path != "") 
                            {
						        return r_path.Replace("res:/",resource_path);
					        };
					        return r_path.Replace("res://", "");
				        }
			        }
		        } break;

		        case AccessType.ACCESS_USERDATA: {
			        if (r_path.StartsWith("user://")) 
                    {
				        string data_dir = OS.singleton.get_data_dir();
				        if (data_dir != "") 
                        {
					        return r_path.Replace("user:/", data_dir);
				        };
				        return r_path.Replace("user://", "");
			        }

		        } break;

		        case AccessType.ACCESS_FILESYSTEM: {
			        return r_path;
		        } break;
	        }
	
	        return r_path;
        }

        public virtual void set_access_type(AccessType p_access)
        {
            __access_type = p_access;
        }

	    protected abstract Error _open(string p_path, int p_mode_flags); ///< open a file
	    
        protected abstract ulong _get_modified_time(string p_file);

        public abstract void close();

        public abstract void is_open();

        public abstract void seek(uint p_position);

        public abstract void seek_end(long p_position = 0);

        public abstract uint get_pos();

        public abstract uint get_len();

        public abstract bool eof_reached();

        public abstract Error get_error();

        public abstract byte get_8();

        public virtual void set_endian_swap(bool p_swap) { _endian_swap = p_swap; }
	    
        public bool get_endian_swap() { return _endian_swap; }

        public virtual ushort get_16()
        {	
	        ushort res;
	        byte a, b;	
	        a = get_8();
	        b = get_8();	
	        if (_endian_swap) 
            {	
		        Misc.swap<byte>( ref a, ref b );
	        }	
	        res = b;
	        res <<= 8;
	        res |= a;	
	        return res;
        }

        public virtual uint get_32()
        {	
	        uint res;
	        ushort a, b;
	
	        a = get_16();
	        b = get_16();
	
	        if (_endian_swap) 
            {	
		        Misc.swap<ushort>(ref a, ref b );
	        }
	
	        res = b;
	        res <<= 16;
	        res |= a;
	
	        return res;	
        }

        public virtual ulong get_64() 
        {	
	        ulong res;
	        uint a, b;
	
	        a = get_32();
	        b = get_32();
	
	        if (_endian_swap) 
            {	
		        Misc.swap<uint>(ref a, ref b );
	        }
	
	        res = b;
	        res <<= 32;
	        res |= a;
	
	        return res;	
	
        }

        public virtual float get_float()
        {
	        Marshalls.MarshallFloat m;
            m.f = 0.0f;
	        m.i = get_32();
	        return m.f;
        }

        public virtual double get_real() 
        {
	        if (real_is_double)
		        return get_double();
	        else
		        return get_float();
        }

        public virtual double get_double()
        {
	        Marshalls.MarshallDouble m;
            m.d = 0;
	        m.l = get_64();
	        return m.d;

        }

        public virtual int get_buffer(ref List<byte> p_dst,int p_length)
        {	
	        int i = 0;
	        for (i = 0; i < p_length && !eof_reached(); i++)
		        p_dst.Add(get_8());
	        return i;
        }

        public virtual int get_buffer(byte[] p_buffer, int p_length)
        {
            int i = 0;
            for (i = 0; i < p_length && !eof_reached(); i++)
                p_buffer[i] = get_8();
            return i;
        }

        public virtual string get_line()
        {
            throw new System.NotImplementedException();
            
        }

        public List<string> get_csv_line()
        {
            throw new System.NotImplementedException();
        }

        public abstract void store_8(byte p_dest);

        public virtual void store_16(ushort p_dest) 
        {	
	        byte a,b;
	
	        a = (byte)( p_dest & 0xFF);
	        b = (byte)(p_dest >> 8);
	
	        if (_endian_swap) 
            {	
		        Misc.swap<byte>( ref a, ref b );
	        }
		
	        store_8(a);
	        store_8(b);
        }

        public virtual void store_32(uint p_dest) 
        {
	        ushort a,b;
	
	        a = (ushort)(p_dest & 0xFFFF);
	        b = (ushort)(p_dest >> 16);
	
	        if (_endian_swap) 
            {	
		        Misc.swap<ushort>(ref a, ref b );
	        }		
	        store_16(a);
	        store_16(b);	
        }

        public virtual void store_64(ulong p_dest) 
        {	
	        uint a,b;
	
	        a = (uint)(p_dest & 0xFFFFFFFF);
	        b = (uint)(p_dest >> 32);
	
	        if (_endian_swap) 
            {	
		        Misc.swap<uint>(ref a, ref b);
	        }
		
	        store_32(a);
	        store_32(b);
	
        }

        public virtual void store_real(double p_real) 
        {
	        //if (sizeof(real_t)==4)
		    //    store_float(p_real);
	        //else
		        store_double(p_real);
        }

        public virtual void store_float(float p_dest) 
        {
	        Marshalls.MarshallFloat m;
            m.i = 0;
	        m.f = p_dest;
	        store_32(m.i);
        }

        public virtual void store_double(double p_dest) 
        {
	        Marshalls.MarshallDouble m;
            m.l = 0;
	        m.d = p_dest;
	        store_64(m.l);
        }

        public virtual void store_string(string p_str)
        {
            throw new System.NotImplementedException();
        }

        public void store_pascal_string(string p_string) 
        {
            throw new System.NotImplementedException();
        }

        public void store_buffer(byte[] p_src, int? len = null) 
        {
            int length = len ?? p_src.Length;
            for (int i = 0; i < length; i++)
		        store_8(p_src[i]);
        }

        public abstract bool file_exists(string p_name);

        public static FileAccess open(string p_path, ModeFlags p_mode_flags, ref Error r_error)
        {
            //try packed data first
	        FileAccess ret = null;
	        if ((p_mode_flags & ModeFlags.WRITE) == 0 && PackedData.singleton != null 
                && !PackedData.singleton.is_disabled()) 
            {
		        ret = PackedData.singleton.try_open_path(p_path);
                if (ret != null) 
                {
			        r_error = Error.OK;
			        return ret;
		        }
	        }

	        ret = create_for_path(p_path);
	        Error err = ret._open(p_path,(int) p_mode_flags);

	        r_error = err;
	        
            if (err != Error.OK) 
            {
		        ret = null;
	        }

	        return ret;
        }

        public static FileAccess create_for_path(string p_path) 
        {
            FileAccess ret = null;

            if (p_path != null)
            {
                if (p_path.StartsWith("res://"))
                {
                    ret = create(AccessType.ACCESS_RESOURCES);
                }
                else if (p_path.StartsWith("user://"))
                {
                    ret = create(AccessType.ACCESS_USERDATA);
                }
                else
                {
                    ret = create(AccessType.ACCESS_FILESYSTEM);
                }
            }
            else
            {
                ret = create(AccessType.ACCESS_FILESYSTEM);
            }

	        return ret;
        }

        public static FileAccess create(AccessType p_access)
        {
	        if(__create_func == null) 
                return null;

	        if( p_access >= AccessType.ACCESS_MAX) 
                return null;

	        FileAccess ret = __create_func[(int)p_access]();
	        ret.set_access_type(p_access);
	        return ret;
        }

        public static bool exists(string p_name) 
        {
	        if (PackedData.singleton.has_path(p_name))
		        return true;
            Error err = Error.OK;
	        FileAccess f = open(p_name, ModeFlags.READ, ref err);
            if (f == null)
                return false;
	        return true;
        }

        public static ulong get_modified_time(string p_file) 
        {
	        FileAccess fa = create_for_path(p_file);
	        if(fa == null)
                return 0;
	        ulong mt = fa._get_modified_time(p_file);
	        return mt;
        }

	    public static void make_default<T>(AccessType p_access)  where T : FileAccess , new() 
        {
		    __create_func[(int)p_access] = __create_builtin<T>;
	    }

        protected bool _endian_swap;

        public bool real_is_double { get; set; }

        private static CreateFunc[] __create_func = new CreateFunc[(int)AccessType.ACCESS_MAX];

        private static bool __backup_save;

        private AccessType __access_type;

       
    }
}
